package file

import (
	"bytes"
	"errors"
	"io"
	"os"
	"path/filepath"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestCheckPKGSignature(t *testing.T) {
	read := func(name string) []byte {
		b, err := os.ReadFile(name)
		require.NoError(t, err)
		return b
	}
	testCases := []struct {
		in  []byte
		out error
	}{
		{in: []byte{}, out: io.EOF},
		{
			in:  read("./testdata/invalid.tar.gz"),
			out: ErrInvalidType,
		},
		{
			in:  read("./testdata/unsigned.pkg"),
			out: ErrNotSigned,
		},
		{
			in:  read("./testdata/signed.pkg"),
			out: nil,
		},
		{
			out: errors.New("decompressing TOC: unexpected EOF"),
			in:  read("./testdata/wrong-toc.pkg"),
		},
	}

	for _, c := range testCases {
		r := bytes.NewReader(c.in)
		err := CheckPKGSignature(r)
		if c.out != nil {
			require.ErrorContains(t, err, c.out.Error())
		} else {
			require.NoError(t, err)
		}
	}
}

func TestParseRealDistributionFiles(t *testing.T) {
	t.Parallel()
	tests := []struct {
		file               string
		expectedName       string
		expectedVersion    string
		expectedBundleID   string
		expectedPackageIDs []string
	}{
		{
			file:               "distribution-1password.xml",
			expectedName:       "1Password",
			expectedVersion:    "8.10.34",
			expectedBundleID:   "com.1password.1password",
			expectedPackageIDs: []string{"com.1password.1password"},
		},
		{
			file:               "distribution-chrome.xml",
			expectedName:       "Google Chrome",
			expectedVersion:    "126.0.6478.62",
			expectedBundleID:   "com.google.Chrome",
			expectedPackageIDs: []string{"com.google.Chrome"},
		},
		{
			file:               "distribution-edge.xml",
			expectedName:       "Microsoft Edge",
			expectedVersion:    "126.0.2592.56",
			expectedBundleID:   "com.microsoft.edgemac",
			expectedPackageIDs: []string{"com.microsoft.edgemac"},
		},
		{
			file:               "distribution-firefox.xml",
			expectedName:       "Firefox",
			expectedVersion:    "99.0",
			expectedBundleID:   "org.mozilla.firefox",
			expectedPackageIDs: []string{"org.mozilla.firefox"},
		},
		{
			file:               "distribution-fleet.xml",
			expectedName:       "Fleet osquery",
			expectedVersion:    "42.0.0",
			expectedBundleID:   "com.fleetdm.orbit",
			expectedPackageIDs: []string{"com.fleetdm.orbit.base.pkg"},
		},
		{
			file:               "distribution-go.xml",
			expectedName:       "Go",
			expectedVersion:    "go1.22.4",
			expectedBundleID:   "org.golang.go",
			expectedPackageIDs: []string{"org.golang.go"},
		},
		{
			file:             "distribution-microsoft-teams.xml",
			expectedName:     "Microsoft Teams",
			expectedVersion:  "24124.1412.2911.3341",
			expectedBundleID: "com.microsoft.teams2",
			expectedPackageIDs: []string{
				"com.microsoft.MSTeamsAudioDevice", "com.microsoft.package.Microsoft_AutoUpdate.app",
				"com.microsoft.teams2",
			},
		},
		{
			file:               "distribution-zoom.xml",
			expectedName:       "zoom.us",
			expectedVersion:    "6.0.11.35001",
			expectedBundleID:   "us.zoom.xos",
			expectedPackageIDs: []string{"us.zoom.pkg.videomeeting"},
		},
		{
			file:             "distribution-acrobatreader.xml",
			expectedName:     "Adobe Acrobat Reader",
			expectedVersion:  "24.002.20857",
			expectedBundleID: "com.adobe.Reader",
			expectedPackageIDs: []string{
				"com.adobe.acrobat.DC.reader.app.pkg.MUI", "com.adobe.acrobat.DC.reader.appsupport.pkg.MUI",
				"com.adobe.acrobat.reader.DC.reader.app.pkg.MUI", "com.adobe.armdc.app.pkg",
			},
		},
		{
			file:               "distribution-airtame.xml",
			expectedName:       "Airtame",
			expectedVersion:    "4.10.1",
			expectedBundleID:   "com.airtame.airtame-application",
			expectedPackageIDs: []string{"com.airtame.airtame-application"},
		},
		{
			file:             "distribution-boxdrive.xml",
			expectedName:     "Box",
			expectedVersion:  "2.38.173",
			expectedBundleID: "com.box.desktop",
			expectedPackageIDs: []string{
				"com.box.desktop.installer.autoupdater", "com.box.desktop.installer.desktop",
				"com.box.desktop.installer.local.appsupport", "com.box.desktop.installer.osxfuse",
			},
		},
		{
			file:             "distribution-iriunwebcam.xml",
			expectedName:     "IriunWebcam",
			expectedVersion:  "2.8.8",
			expectedBundleID: "com.iriun.macwebcam",
			// Note: "com.iriun.pkg.multicam" is part of the installer package, but it is not actually installed by default.
			// We can't reliably determine which packages are installed by the installer, so we just list all of them.
			expectedPackageIDs: []string{"com.iriun.pkg.multicam", "com.iriun.pkg.webcam.tmp"},
		},
		{
			file:             "distribution-microsoftexcel.xml",
			expectedName:     "Microsoft Excel",
			expectedVersion:  "16.86",
			expectedBundleID: "com.microsoft.Excel",
			expectedPackageIDs: []string{
				"com.microsoft.package.Microsoft_AutoUpdate.app", "com.microsoft.package.Microsoft_Excel.app",
				"com.microsoft.pkg.licensing",
			},
		},
		{
			file:             "distribution-microsoftword.xml",
			expectedName:     "Microsoft Word",
			expectedVersion:  "16.86",
			expectedBundleID: "com.microsoft.Word",
			expectedPackageIDs: []string{
				"com.microsoft.package.Microsoft_AutoUpdate.app", "com.microsoft.package.Microsoft_Word.app",
				"com.microsoft.pkg.licensing",
			},
		},
		{
			file:             "distribution-miscrosoftpowerpoint.xml",
			expectedName:     "Microsoft PowerPoint",
			expectedVersion:  "16.86",
			expectedBundleID: "com.microsoft.Powerpoint",
			expectedPackageIDs: []string{
				"com.microsoft.package.Microsoft_AutoUpdate.app", "com.microsoft.package.Microsoft_PowerPoint.app",
				"com.microsoft.pkg.licensing",
			},
		},
		{
			file:               "distribution-ringcentral.xml",
			expectedName:       "RingCentral",
			expectedVersion:    "24.1.32.9774",
			expectedBundleID:   "com.ringcentral.glip",
			expectedPackageIDs: []string{"com.ringcentral.glip"},
		},
		{
			file:               "distribution-zoom-full.xml",
			expectedName:       "Zoom Workplace",
			expectedVersion:    "6.1.1.36333",
			expectedBundleID:   "us.zoom.xos",
			expectedPackageIDs: []string{"us.zoom.pkg.videomeeting"},
		},
		{
			file:               "test-zero-installkbytes.xml",
			expectedName:       "ZeroInstallSize",
			expectedVersion:    "1.2.3",
			expectedBundleID:   "com.bozo.zeroinstallsize",
			expectedPackageIDs: []string{"com.bozo.zeroinstallsize.app"},
		},
		{
			file:               "distribution-sentinelone.xml",
			expectedName:       "SentinelOne",
			expectedVersion:    "24.3.2.7753",
			expectedBundleID:   "com.sentinelone.SentinelAgent",
			expectedPackageIDs: []string{"com.sentinelone.pkg.sentinel-agent", "com.sentinelone.sentinel-agent"},
		},
		{
			file:             "distribution-cold-turkey.xml",
			expectedName:     "Cold Turkey Blocker",
			expectedVersion:  "4.5",
			expectedBundleID: "com.getcoldturkey.coldturkeyblocker",
			expectedPackageIDs: []string{
				"com.getcoldturkey.blocker-chrome-ext", "com.getcoldturkey.blocker-edge-ext",
				"com.getcoldturkey.blocker-firefox-ext", "com.getcoldturkey.coldturkeyblocker",
			},
		},
		{
			file:               "distribution-privileges.xml",
			expectedName:       "Privileges",
			expectedVersion:    "2.4.0",
			expectedBundleID:   "corp.sap.privileges",
			expectedPackageIDs: []string{"corp.sap.privileges.pkg"},
		},
		{
			file:               "distribution-cisco-secure-client.xml",
			expectedName:       "Cisco Secure Client",
			expectedVersion:    "5.1.3.62",
			expectedBundleID:   "com.cisco.secureclient.gui",
			expectedPackageIDs: []string{"com.cisco.pkg.anyconnect.vpn"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.file, func(t *testing.T) {
			rawXML, err := os.ReadFile(filepath.Join("testdata", "distribution", tt.file))
			require.NoError(t, err)
			metadata, err := parseDistributionFile(rawXML)
			require.NoError(t, err)
			assert.Equal(t, tt.expectedPackageIDs, metadata.PackageIDs)
			require.Equal(t, tt.expectedName, metadata.Name)
			require.Equal(t, tt.expectedVersion, metadata.Version)
			require.Equal(t, tt.expectedBundleID, metadata.BundleIdentifier)
		})
	}
}

func TestParsePackageInfoFiles(t *testing.T) {
	t.Parallel()
	tests := []struct {
		file               string
		expectedName       string
		expectedVersion    string
		expectedBundleID   string
		expectedPackageIDs []string
	}{
		{
			file:               "packageInfo-oktaVerify.xml",
			expectedName:       "Okta Verify",
			expectedVersion:    "9.27.0",
			expectedBundleID:   "com.okta.mobile",
			expectedPackageIDs: []string{"com.okta.mobile"},
		},
		{
			file:             "packageInfo-iriunWebcam.xml",
			expectedName:     "IriunWebcam",
			expectedVersion:  "2.8.10",
			expectedBundleID: "com.iriun.macwebcam",
			expectedPackageIDs: []string{
				"com.iriun.macwebcam", "com.iriun.macwebcam.extension",
				"com.iriun.macwebcam.extension4", "com.iriun.mic",
			},
		},
		{
			file:               "packageinfo-subEthaEdit-modded.xml",
			expectedName:       "SubEthaEdit",
			expectedVersion:    "5.2.4",
			expectedBundleID:   "de.codingmonkeys.SubEthaEdit.MacFULL",
			expectedPackageIDs: []string{"de.codingmonkeys.SubEthaEdit.MacFULL"},
		},
		{
			file:               "packageInfo-scriptOnly.xml",
			expectedName:       "HelloWorld",
			expectedVersion:    "1.2.3",
			expectedBundleID:   "com.mygreatcompany.pkg.HelloWorld",
			expectedPackageIDs: []string{"com.mygreatcompany.pkg.HelloWorld"},
		},
		{
			file:               "packageInfo-empty.xml",
			expectedName:       "",
			expectedVersion:    "",
			expectedBundleID:   "",
			expectedPackageIDs: []string(nil),
		},
		{
			file:               "packageInfo-versionOnly.xml",
			expectedName:       "",
			expectedVersion:    "test-version",
			expectedBundleID:   "",
			expectedPackageIDs: []string(nil),
		},
	}

	for _, tt := range tests {
		t.Run(tt.file, func(t *testing.T) {
			rawXML, err := os.ReadFile(filepath.Join("testdata", "packageInfo", tt.file))
			require.NoError(t, err)
			metadata, err := parsePackageInfoFile(rawXML)
			require.NoError(t, err)
			assert.Equal(t, tt.expectedPackageIDs, metadata.PackageIDs)
			assert.Equal(t, tt.expectedName, metadata.Name)
			assert.Equal(t, tt.expectedVersion, metadata.Version)
			assert.Equal(t, tt.expectedBundleID, metadata.BundleIdentifier)
		})
	}
}

func TestIsValidAppFilePath(t *testing.T) {
	tests := []struct {
		input    string
		expected bool
	}{
		{"baz.app", true},
		{"foo/bar/baz.app", false},
		{"Applications/baz.app", true},
		{"Applications/foo/baz.app", true},
		{"foo/baz.app", false},
		{"baz.txt", true},
		{"Applications/baz.txt", false},
		{"Applications/Cisco/Cisco Secure Client.app", true},
		{"Applications/Foo with spaces.app", true},
		{"Applications/foo", false},
		{"foo", true},
	}

	for _, test := range tests {
		_, ok := isValidAppFilePath(test.input)
		require.Equal(t, test.expected, ok, test.input)
	}
}
